package scanner

import (
  "bufio"
  "unicode"
  "strings"
)

// Constants for tokens
const (
  None   byte = iota
  Number      // Digit{Digit}
  Ident       // Letter{Letter|Digit}
  Add         // '+'
  Sub         // '-'
  Mul         // '*'
  Div         // '/'
  LPar        // '('
  RPar        // ')'
  Semic       // ';'
  Period      // '.'
  Equal       // '='
  NotEq       // '<>'
  Great       // '>'
  GrEq        // '>='
  Less        // '<'
  LsEq        // '<='
  Print       // 'print'
  Input       // 'input'
  If          // 'if'
  Then        // 'then'
  ElseIf      // 'elseif'
  Else        // 'else'
  While       // 'while'
  Do          // 'do'
  End         // 'end'
  Eos
)

var keywords = map[string]byte{
  "print":  Print,
  "input":  Input,
  "if":     If,
  "then":   Then,
  "elseif": ElseIf,
  "else":   Else,
  "while":  While,
  "do":     Do,
  "end":    End,
}

var operations = map[string]byte{
  "+":  Add,
  "-":  Sub,
  "*":  Mul,
  "/":  Div,
  "=":  Equal,
  "<>": NotEq,
  ">":  Great,
  ">=": GrEq,
  "<":  Less,
  "<=": LsEq,
}

// Scanner contains array of characters and current position
type Scanner struct {
  source *bufio.Reader
  line   int
}

func New(src *bufio.Reader) *Scanner {
  return &Scanner{src, 1}
}

func (s *Scanner) peek() rune {
  br, ok := s.source.Peek(1)
  if ok != nil {
    return 0
  }
  return rune(br[0])
}

func (s *Scanner) char() rune {
  ch, _, ok := s.source.ReadRune()
  if ok != nil {
    return 0
  }
  return ch
}

// Helper method to scan characters with given predicate
func (s *Scanner) scanWith(predicate func(r rune) bool) string {
  res := ""
  ch := s.char()
  for predicate(ch) {
    res += string(ch)
    if ch == '\n' {
      s.line++
    }
    ch = s.char()
  }
  s.source.UnreadRune()
  return res
}

func isLetterOrDigit(r rune) bool {
  return unicode.IsLetter(r) || unicode.IsDigit(r)
}

func isOpChar(r rune) bool {
  return strings.ContainsRune("+-*/=><", r)
}

// Scan new lexeme and return it with token
func (s *Scanner) Next() (string, byte) {
  // skip whitespaces
  s.scanWith(unicode.IsSpace)

  ch := s.peek()
  if ch == 0 {
    return "", Eos
  }

  // Scan numbers
  if unicode.IsDigit(ch) {
    return s.scanWith(unicode.IsDigit), Number
  }

  // identifiers
  if unicode.IsLetter(ch) {
    iden := s.scanWith(isLetterOrDigit)
    tok, iskey := keywords[iden]
    if !iskey {
      tok = Ident
    }
    return iden, tok
  }

  // operations
  if isOpChar(ch) {
    str := s.scanWith(isOpChar)
    tok, iskey := operations[str]
    if !iskey {
      tok = None
    }
    return str, tok
  }

  // scan symbols
  ch = s.char()
  var res byte = None
  switch ch {
    case '(':
      res = LPar
    case ')':
      res = RPar
    case ';':
      res = Semic
    case '.':
      res = Period
  }

  return string(ch), res
}


